package org.beanutopia.function;

import static org.beanutopia.function.PropertyFunctions.ifNull;
import static org.beanutopia.reflect.Reflector.reader;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Nullable;
import com.google.common.base.Predicate;
import static com.google.common.collect.Iterables.addAll;
import static com.google.common.collect.ObjectArrays.newArray;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import static java.util.Arrays.asList;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * @author ymeymann
 * @since Nov 14, 2007 11:22:00 AM
 */
public class CompositeFunction<A,B> implements Function<A,B> {
  final Function<A,B> function;
  final boolean isNullSupported;

  private CompositeFunction(Function<A,B> f){
    function = f;
    isNullSupported = isNullSupported(function);
  }

  public <C> CompositeFunction<A,C> compose(final Function<? super B,C> f) {
    if (isNullSupported(f)) {
      return new CompositeFunction<A,C>(new Function<A,C>(){
        public C apply(@Nullable A from) {
          return f.apply(CompositeFunction.this.apply(from));
        }
      });
    } else {
      return new CompositeFunction<A,C>(new Function<A,C>(){
        public C apply(A from) {
          return f.apply(CompositeFunction.this.apply(from));
        }
      });
    }
  }

  public CompositeFunction<A,B> or(final B defaultValue) {
    return compose(ifNull(defaultValue));
  }

  public <C extends B> CompositeFunction<A,C> cast(Class<C> c) {
    return compose(Cast.cast(c));
  }

  public CompositeFunction<A,Object> reflect(String propertyName) {
    return compose(reader(propertyName));
  }

  public CompositeFunction<A,Object> reflect(String propertyName, String prefix) {
    return compose(reader(propertyName, prefix));
  }

  public Predicate<A> condition(final Predicate<? super B> p) {
    return new Predicate<A>() {
      public boolean apply(A a) {
        return p.apply(CompositeFunction.this.apply(a));
      }
    };
  }

  public <C> Injection<A,C> inject(final Injection<? super B,C> inject) {
    return new Injection<A,C>() {
      public void apply(A object, C value) {
        inject.apply(CompositeFunction.this.apply(object), value);
      }
    };
  }

  public B apply(A from) {
    return function.apply(from); 
  }

  /**
   * With this method, instead of CompositeFunction.&lt;Foo&gt;self() one can write self(Foo.class)
   */
  public static <S> CompositeFunction<S,S> self(Class<S> c) {
    return self(true);
  }

  public static <S> CompositeFunction<S,S> self(Class<S> c, boolean nullSafe) {
    return self(nullSafe);
  }

  public static <S> CompositeFunction<S,S> self() {
    return self(true);
  }

  public static <A,B> CompositeFunction<A,B> self(Function<A,B> f) {
    return self(f, true);
  }

  public static <A,B> CompositeFunction<A,B> self(Function<A,B> f, boolean nullSafe) {
    return nullSafe? new NullSafe<A,B>(f) : new CompositeFunction<A,B>(f);
  }

  public static <S> CompositeFunction<S,S> self(boolean nullSafe) {
    return nullSafe ?
            new NullSafe<S,S>(Functions.<S>identity()) :
            new CompositeFunction<S,S>(Functions.<S>identity());
  }

  public static boolean isNullSupported(Function<?,?> function) {

    //TODO cache this per compose class

    Type functionInt = findGenericFunctionAncestor(function);
    Class[] genericParams = getGenericParameterClasses(functionInt, 1);
//    Class[] genericParams = getGenericParameterClasses(functionInt);
    if (genericParams.length == 0) genericParams = new Class[] {Object.class};
//    if (genericParams.length < 2) genericParams = new Class[] {Object.class, Object.class};
    Method applyMethod = null;
    try {
      applyMethod = function.getClass().getMethod("apply", genericParams[0]);
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    }
//    Method[] allMethods = compose.getClass().getMethods();
//    for (Method method : allMethods) {
//      if (method.getName().equals("apply")) {
//        Class[] params = method.getParameterTypes();
//        if (params != null && params.length == 1 &&
//            params[0].equals(genericParams[0]) &&
//            method.getReturnType().equals(genericParams[1])) {
//          applyMethod = method;
//          break;
//        }
//      }
//    }
    List<Annotation> l = asList(applyMethod.getParameterAnnotations()[0]);
    for (Annotation a: l) {
      if (a.annotationType().equals(Nullable.class)) return true;
    }
    return false; 
  }

  public static Type findGenericFunctionAncestor(Function<?, ?> function) {
    Class<?> curClass = function.getClass();
    Set<Type> allInt = new LinkedHashSet<Type>();
    while (curClass != Object.class) {
      Type[] ints = curClass.getGenericInterfaces();
      if (ints != null) {
        addAll(allInt,  asList(ints));
      }
      curClass = curClass.getSuperclass();
    }
    Type functionInt = null;
    for (Type t: allInt) {
      if ((t instanceof Class && t == Function.class) || (t instanceof ParameterizedType && ((ParameterizedType)t).getRawType() == Function.class)) {
        functionInt = t;
        break;
      }
    }
    return functionInt == null ? Function.class : functionInt;
  }

  public static Class[] getGenericParameterClasses(Type superType) {
    return getGenericParameterClasses(superType, Integer.MAX_VALUE);
  }

  public static Class[] getGenericParameterClasses(Type superType, int parameterCount) {
    //see http://gafter.blogspot.com/2006/12/super-type-tokens.html
    List<Class<?>> res = new LinkedList<Class<?>>();
    if (!(superType instanceof Class)) {
      ParameterizedType paramSuperclass = (ParameterizedType) superType;
      Type[] args = paramSuperclass.getActualTypeArguments();
      int i = 0;
      for (Type arg: args) {
        if (i >= parameterCount) break;
        if (arg instanceof Class<?>) {
          res.add((Class<?>) arg);
        } else if (arg instanceof ParameterizedType) {
          res.add((Class<?>) ((ParameterizedType) arg).getRawType());
        } else if (arg instanceof GenericArrayType) {
          Type elemType = ((GenericArrayType) arg).getGenericComponentType();
          //TODO support for multi-dimensional arrays
          Class<?> elemClass = Object.class;
          if (elemType instanceof Class<?>)
            elemClass = (Class<?>) elemType;
          else if (elemType instanceof ParameterizedType)
            elemClass = (Class<?>)((ParameterizedType)elemType).getRawType();
          res.add(Array.newInstance(elemClass ,0).getClass());
        } else {
          res.add(Object.class);
        }
        i++;
      } //for
    }
    return res.toArray(newArray(Class.class, res.size()));
  }

  
  public static class NullSafe<A,B> extends CompositeFunction<A,B> {
    private NullSafe(Function<A,B> f){
      super(f);
    }

    @Override
    public <C> NullSafe<A,C> compose(final Function<? super B,C> f) {
      if (isNullSupported(f)) {
        return new NullSafe<A,C>(new Function<A,C>(){
          public C apply(@Nullable A from) {
            B res = NullSafe.this.apply(from);
            return f.apply(res);
          }
        });
      } else {
        return new NullSafe<A,C>(new Function<A,C>(){
          public C apply(A from) {
            B res = NullSafe.this.apply(from);
            return res == null ? null : f.apply(res);
          }
        });
      }
    }

    @Override
    public Predicate<A> condition(final Predicate<? super B> p) {
      return new Predicate<A>() {
        public boolean apply(A a) {
          B res = NullSafe.this.apply(a);
          return res!= null && p.apply(res);
        }
      };
    }

    @Override
    public <C> Injection<A,C> inject(final Injection<? super B,C> inject) {
      return new Injection<A,C>() {
        public void apply(A object, C value) {
          B res = NullSafe.this.apply(object);
          if (res != null) inject.apply(res, value);
        }
      };
    }

    @Override
    public B apply(@Nullable A from) {
      if (from == null && !isNullSupported) return null;
      return function.apply(from); 
    }

  }

}
